//
//  BMJson.m
//  MacSnome
//
//  Created by Daniel Hazelbaker on 5/23/09.
//  Copyright 2009 Blue Box Moon. All rights reserved.
//

#import "BMJson.h"


//
// TODO: Support exponents in numbers.
//


@interface BMJson (BMJsonPrivate)

+ (id)decodeJsonElement:(NSString *)element consumed:(int *)consumedLength;
+ (NSString *)decodeJsonString:(NSString *)element consumed:(int *)consumedLength;
+ (NSNumber *)decodeJsonNumber:(NSString *)element consumed:(int *)consumedLength;
+ (NSDictionary *)decodeJsonDictionary:(NSString *)element consumed:(int *)consumedLength;
+ (NSArray *)decodeJsonArray:(NSString *)element consumed:(int *)consumedLength;

+ (NSString *)trimLeadingWhitespace:(NSString *)json;

+ (NSString *)encodeJsonString:(NSString *)string;
+ (NSString *)encodeJsonNumber:(NSNumber *)number;
+ (NSString *)encodeJsonDictionary:(NSDictionary *)dictionary;
+ (NSString *)encodeJsonArray:(NSArray *)array;

@end

@implementation BMJson


#pragma mark Public methods for other classes to call.

//////////////////////////////////////////////////////////////////////////////////////
//
// Public methods for other classes to call.
//
//////////////////////////////////////////////////////////////////////////////////////

//
// Decode the Json string into the native object.
//
+ (id)decodeJson:(NSString *)json
{
    return [self decodeJsonElement:json consumed:nil];
}


//
// Encode the native object into a valid Json string for transmission to a remote decoder.
//
+ (NSString *)encodeObject:(id)object
{
    if ([object isKindOfClass:[NSString class]])
	return [self encodeJsonString:object];
    else if ([object isKindOfClass:[NSNumber class]])
	return [self encodeJsonNumber:object];
    else if ([object isKindOfClass:[NSDictionary class]])
	return [self encodeJsonDictionary:object];
    else if ([object isKindOfClass:[NSArray class]])
	return [self encodeJsonArray:object];
    else if ([object isKindOfClass:[NSNull class]])
	return @"null";
    else
	[NSException raise:NSInvalidArgumentException format:@"Cannot encode unknown object for JSON serialization."];

    return nil;
}



#pragma mark Private methods used for decoding various object types.

//////////////////////////////////////////////////////////////////////////////////////
//
// Private methods used for decoding various object types.
//
//////////////////////////////////////////////////////////////////////////////////////

//
// Decode the Json string element into it's native object, optionally returning the
// number of characters consumed in the transformation.
//
+ (id)decodeJsonElement:(NSString *)element consumed:(int *)consumedLength
{
    NSString *json;
    char     ch;
    int	     trimmed;
    id	     object;


    //
    // Trim whitespace.
    //
    json = [self trimLeadingWhitespace:element];
    trimmed = ([element length] - [json length]);

    //
    // Check for bad data.
    //
    if ([element length] == 0)
	return nil;

    //
    // Get the character and determine the type of object.
    //
    ch = [element characterAtIndex:0];
    if (ch == '{') {
	//
	// Object is a dictionary.
	//
	object = [self decodeJsonDictionary:element consumed:consumedLength];
	if (consumedLength != nil)
	    *consumedLength += trimmed;

	return object;
    }
    else if (ch == '[') {
	//
	// Object is an array.
	//
	object = [self decodeJsonArray:element consumed:consumedLength];
	if (consumedLength != nil)
	    *consumedLength += trimmed;

	return object;
    }
    else if (ch == '"') {
	//
	// Object is a litteral string (or a date object).
	//
	object = [self decodeJsonString:element consumed:consumedLength];
	if (consumedLength != nil)
	    *consumedLength += trimmed;

	return object;
    }
    else if ((ch >= '0' && ch <= '9') || ch == '-') {
	//
	// Object is a number.
	//
	object = [self decodeJsonNumber:element consumed:consumedLength];
	if (consumedLength != nil)
	    *consumedLength += trimmed;

	return object;
    }
    else if ([element length] >= 4 && [element compare:@"true" options:NSCaseInsensitiveSearch range:NSMakeRange(0, 4)] == NSOrderedSame) {
	//
	// Object is a true value.
	//
	object = [NSNumber numberWithBool:YES];
	if (consumedLength != nil)
	    *consumedLength = (trimmed + 4);

	return object;
    }
    else if ([element length] >= 5 && [element compare:@"false" options:NSCaseInsensitiveSearch range:NSMakeRange(0, 5)] == NSOrderedSame) {
	//
	// Object is a false value.
	//
	object = [NSNumber numberWithBool:NO];
	if (consumedLength != nil)
	    *consumedLength = (trimmed + 5);

	return object;
    }
    else if ([element length] >= 4 && [element compare:@"null" options:NSCaseInsensitiveSearch range:NSMakeRange(0, 4)] == NSOrderedSame) {
	//
	// Object is null.
	//
	object = [NSNull null];
	if (consumedLength != nil)
	    *consumedLength = (trimmed + 4);

	return object;
    }
    else
	[NSException raise:NSInvalidArgumentException format:@"The JSON element was not recognized."];

    return nil;
}


//
// Decode a Json sring element into the native NSString object.
//
+ (NSString *)decodeJsonString:(NSString *)element consumed:(int *)consumedLength
{
    NSMutableString *string = [NSMutableString string];
    char	    ch;
    int		    i;


    //
    // Skip the first character which is a " mark, all leading whitespace has
    // already been stripped.
    //
    for (i = 1; i < [element length]; i++) {
	ch = [element characterAtIndex:i];

	//
	// Check for an escape character and deal with it. We don't handle
	// every kind of escape character, at-least I don't think we do, but
	// we handle probably 99.99% of them.
	//
	if (ch == '\\') {
	    ch = [element characterAtIndex:++i];
	    if (ch == '\\')
		[string appendString:@"\\"];
	    else if (ch == '"')
		[string appendString:@"\""];
	    else if (ch == '/')
		[string appendString:@"/"];
	    else if (ch == '\b')
		[string appendString:@"\b"];
	    else if (ch == '\f')
		[string appendString:@"\f"];
	    else if (ch == '\n')
		[string appendString:@"\n"];
	    else if (ch == '\r')
		[string appendString:@"\r"];
	    else if (ch == '\t')
		[string appendString:@"\t"];
	    else if (ch == 'u') {
		int uch = 0;

		i++;
		sscanf([[element substringWithRange:NSMakeRange(i, 4)] UTF8String], "%x", &uch);
		i += 3; // 4-1 for the next i
		[string appendFormat:@"%c", uch];
	    }
	    else
		[string appendFormat:@"%c", ch];
	}
	else if (ch == '"') {
	    //
	    // End of string marker.
	    //
	    break;
	}
	else
	    [string appendFormat:@"%c", ch];
    }

    //
    // If the caller wants to know how many bytes we consumed then set the value.
    //
    if (consumedLength != nil)
	*consumedLength = (i + 1);

    if ([string length] == 20 && [string characterAtIndex:10] == 'T' && [string characterAtIndex:19] == 'Z') {
	NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];

	[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
	[formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
	return (id)[formatter dateFromString:string];
    }

    return string;
}


//
// Decode the Json string into a native NSNumber object.
//
+ (NSNumber *)decodeJsonNumber:(NSString *)element consumed:(int *)consumedLength
{
    char *ptr, *endPtr = nil;
    double value;


    //
    // Get the character string to convert. This is not exactly the most efficient
    // way to go about this, as if it is a large string object with a lot of numbers
    // then we could be doing a lot of malloc/free overhead.
    //
    ptr = (char *)[element UTF8String];

    //
    // Convert the value into an NSNumber, strtod should handle exponents.
    //
    value = strtod(ptr, &endPtr);

    //
    // If the caller wants to know how many bytes we consumed them set the value.
    //
    if (endPtr != nil && consumedLength != nil)
	*consumedLength = (int)(endPtr - ptr);
	
    return [NSNumber numberWithDouble:[element doubleValue]];
}


//
// Decode the json string into a NSDictionary object.
//
+ (NSDictionary *)decodeJsonDictionary:(NSString *)element consumed:(int *)consumedLength
{
    NSMutableDictionary	*dictionary = [NSMutableDictionary dictionary];
    NSString		*json;
    NSString		*key;
    int			consumed;
    id			value;


    //
    // Trim any whitespace.
    //
    json = [self trimLeadingWhitespace:[element substringFromIndex:1]];

    //
    // Look for end of dictionary marker, we have to check here to see if we have an
    // empty dictionary.
    //
    if ([json characterAtIndex:0] == '}') {
	json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
	if (consumedLength != nil)
	    *consumedLength = ([element length] - [json length]);

	return dictionary;
    }

    //
    // Walk the dictionary data until we have all the key/value pairs.
    //
    for (;;) {
	//
	// Get the key.
	//
	key = [self decodeJsonString:json consumed:&consumed];
	json = [self trimLeadingWhitespace:[json substringFromIndex:consumed]];

	//
	// Check for key/value separator.
	//
	if ([json characterAtIndex:0] != ':')
	    [NSException raise:NSInternalInconsistencyException format:@"Found unexpected character while looking for JSON dictionary key/value separator."];
	json = [self trimLeadingWhitespace:[json substringFromIndex:1]];

	//
	// Get the value.
	//
	value = [self decodeJsonElement:json consumed:&consumed];
	json = [self trimLeadingWhitespace:[json substringFromIndex:consumed]];
	[dictionary setObject:value forKey:key];
	
	//
	// Look for end of dictionary marker.
	//
	if ([json characterAtIndex:0] == '}') {
	    json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
	    if (consumedLength != nil)
		*consumedLength = ([element length] - [json length]);

	    return dictionary;
	}

	//
	// Otherwise verify we have a pair separator.
	//
	if ([json characterAtIndex:0] != ',')
	    [NSException raise:NSInternalInconsistencyException format:@"Found unexpected character while looking for JSON dictionary pair separator."];
	json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
    }
    
    return nil;
}


//
// Decode the Json string into a NSArray object.
//
+ (NSArray *)decodeJsonArray:(NSString *)element consumed:(int *)consumedLength
{
    NSMutableArray  *array = [NSMutableArray array];
    NSString	    *json;
    int		    consumed;
    id		    value;


    //
    // Trim any whitespace.
    //
    json = [self trimLeadingWhitespace:[element substringFromIndex:1]];

    //
    // Look for end of array marker, we need to check before we begin to see if we have an
    // empty array.
    //
    if ([json characterAtIndex:0] == ']') {
	json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
	if (consumedLength != nil)
	    *consumedLength = ([element length] - [json length]);

	return array;
    }

    //
    // Walk the dictionary data until we have all the key/value pairs.
    //
    for (;;) {
	//
	// Get the value.
	//
	value = [self decodeJsonElement:json consumed:&consumed];
	json = [self trimLeadingWhitespace:[json substringFromIndex:consumed]];
	[array addObject:value];

	//
	// Look for end of array marker.
	//
	if ([json characterAtIndex:0] == ']') {
	    json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
	    if (consumedLength != nil)
		*consumedLength = ([element length] - [json length]);

	    return array;
	}

	//
	// Otherwise verify we have a pair separator.
	//
	if ([json characterAtIndex:0] != ',')
	    [NSException raise:NSInternalInconsistencyException format:@"Found unexpected character while looking for JSON array separator."];
	json = [self trimLeadingWhitespace:[json substringFromIndex:1]];
    }
    
    return nil;
}



#pragma mark Private methods for use in encoding various object types.

///////////////////////////////////////////////////////////////////////////////
//
// Private methods for use in encoding various object types.
//
///////////////////////////////////////////////////////////////////////////////


//
// Encode the NSString object into a Json stream.
//
+ (NSString *)encodeJsonString:(NSString *)string
{
    NSMutableString *newString = [NSMutableString string];
    unsigned char   ch;
    int		    i;


    //
    // Begin the Json string.
    //
    [newString appendString:@"\""];

    //
    // Loop through each character of the original string and encode each
    // one.
    //
    for (i = 0; i < [string length]; i++) {
	ch = [string characterAtIndex:i];

	//
	// Encode any special characters.
	//
	if (ch == '\\')
	    [newString appendString:@"\\\\"];
	else if (ch == '"')
	    [newString appendString:@"\\\""];
	else if (ch == '/')
	    [newString appendString:@"\\/"];
	else if (ch == '\b')
	    [newString appendString:@"\\b"];
	else if (ch == '\f')
	    [newString appendString:@"\\f"];
	else if (ch == '\n')
	    [newString appendString:@"\\n"];
	else if (ch == '\r')
	    [newString appendString:@"\\r"];
	else if (ch == '\t')
	    [newString appendString:@"\\t"];
	else if (ch >= 32 && ch <= 127) {
	    //
	    // A regular character is encoded as-is.
	    //
	    [newString appendFormat:@"%c", ch];
	}
	else {
	    //
	    // Any remaining unknown characters are encoded as escaped hex codes.
	    //
	    [newString appendFormat:@"\\u%04x", ch];
	}
    }

    //
    // Terminate the Json string.
    //
    [newString appendString:@"\""];

    return newString;
}


//
// Encode the NSNumber object into a Json stream.
//
+ (NSString *)encodeJsonNumber:(NSNumber *)number
{
    //
    // Check if this is a true/false object type.
    //
    if ([number objCType][0] == @encode(BOOL)[0]) {
	if ([number boolValue] == YES)
	    return @"true";
	else
	    return @"false";
    }

    return [number stringValue];
}


//
// Encode a NSDictionary object into a Json stream.
//
+ (NSString *)encodeJsonDictionary:(NSDictionary *)dictionary
{
    NSMutableString *string = [NSMutableString string];
    NSArray	    *keys;
    int		    i;


    //
    // Begin the dictionary stream.
    //
    [string appendString:@"{"];
    keys = [dictionary allKeys];

    //
    // Loop through each of the keys.
    //
    for (i = 0; i < [keys count]; i++) {
	//
	// Append each key/value pair to the stream.
	//
	[string appendFormat:@"%@:%@", [self encodeObject:[keys objectAtIndex:i]], [self encodeObject:[dictionary objectForKey:[keys objectAtIndex:i]]]];

	//
	// If we have more, add a separator.
	//
	if ((i + 1) < [keys count])
	    [string appendString:@","];
    }

    //
    // Finish off the stream.
    //
    [string appendString:@"}"];
    
    return string;
}


//
// Encode a NSArray object into a Json stream.
//
+ (NSString *)encodeJsonArray:(NSArray *)array
{
    NSMutableString	*string = [NSMutableString string];
    int			i;


    //
    // Begin the array stream.
    //
    [string appendString:@"["];

    //
    // Loop through each array object.
    //
    for (i = 0; i < [array count]; i++) {
	//
	// Encode and append the object.
	//
	[string appendString:[self encodeObject:[array objectAtIndex:i]]];

	//
	// If we have more, add a separator.
	//
	if ((i + 1) < [array count])
	    [string appendString:@","];
    }

    //
    // Finish off the stream.
    //
    [string appendString:@"]"];
    
    return string;
}



#pragma mark Support methods.

///////////////////////////////////////////////////////////////////////////////
//
// Support methods.
//
///////////////////////////////////////////////////////////////////////////////

//
// Trim off any leading whitespace characters from the Json string. Returns a new
// NSString object.
//
+ (NSString *)trimLeadingWhitespace:(NSString *)json
{
    NSMutableString *string = [NSMutableString stringWithString:json];
    int		    ch;


    //
    // Loop through the string deleting any whitespace characters one at a time.
    //
    for (; [string length] > 0; ) {
	ch = [string characterAtIndex:0];
	if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n')
	    break;

	[string replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];
    }
    
    return string;
}


@end
